<?php
require 'FileParseMessage.class.php';
require 'ClassParseMessage.class.php';
require 'FunctionParseMessage.class.php';

class Parser {
    const LEVEL_ERROR = 0;
    const LEVEL_WARNING = 1;
    const LEVEL_INFO = 2;
    
    private $filename = null; 
    private $tokens = array();
    private $lenght = 0;
    private $index = -1;

    private $currentLine = 0;

    private $messages = array();
    private $fileParse = null;
    private $classParse = null;
    private $functionParse = null;

    private $indent = 0;
    private $classIndent = null;
    private $functionIndent = null;

    private $_errors = array();
    
    private $config = null;

    public function sendMessage($title, $msg = null) {
        foreach( $this->messages as $message ) {
            $message->reveive($title, $msg);
        }
    }

    public function error($line, $msg, $level = self::LEVEL_ERROR) {
        static $from = array('à', 'ä', 'â', 'é', 'è', 'ê', 'ë', 'ï', 'î', 'ô', 'ö', 'ù', 'ü', 'û', 'ç');
        static $to =   array('a', 'a', 'a', 'e', 'e', 'e', 'e', 'i', 'i', 'o', 'o', 'u', 'u', 'u', 'c');
        $msg = str_replace($from, $to, $msg);
        $this->_errors[] = array( $level, $line, $msg );
    }
    
    public function warning($line, $msg) {
        $this->error($line, $msg, self::LEVEL_WARNING);
    }
    
    public function info($line, $msg) {
        $this->error($line, $msg, self::LEVEL_INFO);
    }
    
    public function getErrors() {
        usort( $this->_errors, create_function('$a,$b', 'return $a[1] - $b[1];' ));
        return $this->_errors;
    }
    
    public function checkConf($section, $name) {
        if( isset( $this->config[$section][$name])) {
            $value = $this->config[$section][$name];
            if( preg_match('`^(on|true|yes)$`i', $value)) return true;
            if( preg_match('`^(off|false|no)$`i', $value)) return false;
            return $value;
        } else {
            return false;
        }
    }

    public function __construct() {
        $this->fileParse = new FileParseMessage($this);
        $this->classParse = new ClassParseMessage($this);
        $this->functionParse = new FunctionParseMessage($this);

        $this->messages[] = $this->fileParse;
        $this->messages[] = $this->classParse;
        $this->messages[] = $this->functionParse;
        
        $this->config = parse_ini_file('config.ini', true);
    }
    public function load($file) {
        $this->tokens = token_get_all( file_get_contents($file));
        $this->filename = $file;
        $this->index = -1;
        $this->lenght = count( $this->tokens );
        $this->currentLine = 0;
        $this->indent = -1;
        $this->classIndent = null;
        $this->functionIndent = null;
        foreach( $this->messages as $message) {
            $message->reset();
        }
        $this->fileParse->setFilename($file);
    }

    public function parse() {
        $this->indent++;
        while( $this->index < $this->lenght - 1 ) {
            $token = $this->getNextToken();
            // Comptage des lignes
            if( $token[2] !== $this->currentLine) {
                switch( $token[0]) {
                    case T_COMMENT:
                    case T_DOC_COMMENT:
                        $this->addComment($token);
                    case T_CLOSE_TAG:
                    case T_OPEN_TAG:
                    case T_INLINE_HTML:
                    case T_WHITESPACE:
                    case 0:
                        break;
                    default:
                        $this->addLine($token);
                }
            }

            switch( $token[0] ) {
                case T_CLASS:
                    $this->enterClass();
                    break;
                case T_FUNCTION:
                    if( $this->classIndent !== null) $this->enterMethod();
                    else $this->enterFunction();
                    break;
                case T_IF:
                    $this->processIf();
                    break;
                case T_FOR:
                    $this->processFor();
                    break;
                case T_FOREACH:
                    $this->processForeach();
                    break;
                case T_WHILE:
                    $this->processWhile();
                    break;
                case T_CASE:
                    $this->processCase();
                    break;
                case T_VARIABLE:
                    $this->processVariable($token);
                    break;
                case T_REQUIRE:
                case T_REQUIRE_ONCE:
                case T_INCLUDE:
                case T_INCLUDE_ONCE:
                    $this->parseInclude();
                    break;
                case T_DOUBLE_COLON:
                    $this->addStaticClassReference();
                    break;
                case T_NEW:
                    $this->addNewClassReference();
                    break;
                case T_CATCH:
                    $this->addCatchClassReference();
                    break;
                case T_STRING:
                    $this->processString( $token );
                    break; 
            }

            if( $token[0] === 0) {
                if( $token[1] === '{') {
                    $this->parse();
                } elseif($token[1] === '}') {
                    $this->indent--;
                    $this->processUnIndent();
                    return;
                } elseif( $token[1] === ',') {
                    $next = $this->getToken(1);
                    if( $next[0] !== T_WHITESPACE ) {
                        if( $this->checkConf('spaces', 'space.after.comma')) {
                            $this->error($token[2], 'Il faut un espace après la virgule');
                        }
                    }
                }
            }
        }
    }
    
    public function findErrors() {
        $lines = file( $this->filename );
        $length = $this->checkConf('file', 'line.length');
        if( false !== $length) {
            $length = intval($length);
            $i = 0;
            foreach($lines as $line) {
                $i++;
                $line = trim( $line );
                if( strlen( $line ) > $length ) $this->error($i, 'Ligne trop longue : '.strlen( $line ).' (Max ' . $length . ')');
            }
        }

        $stats = $this->fileParse->getStats();
        
        if( isset($stats['classes'])) {
            $maxClassLines = $this->checkConf('classes', 'lines.count.high');
            $mediumClassLines = $this->checkConf('classes', 'lines.count.medium');
            $maxClassRef = $this->checkConf('classes', 'reference.high');
            $mediumClassRef = $this->checkConf('classes', 'reference.medium');
            $maxClassCyclo = $this->checkConf('classes', 'cyclo.high');
            $mediumClassCyclo = $this->checkConf('classes', 'cyclo.medium');
            
            foreach($stats['classes'] as $name => $stat) {
                if( $maxClassLines !== false and $stat['linesCount'] > $maxClassLines) {
                    $msg = 'La classe "%s" contient trop de lignes de code : %d lignes (max %d)';
                    $this->warning($stat['line'], sprintf($msg, $name, $stat['linesCount'], $maxClassLines));
                } elseif($mediumClassLines !== false and $stat['linesCount'] > $mediumClassLines) {
                    $msg = 'La classe "%s" contient beaucoup de lignes de code : %d lignes';
                    $this->info($stat['line'], sprintf($msg, $name, $stat['linesCount']));
                }
                if( $maxClassRef !== false and count($stat['references']) > $maxClassRef) {
                    $msg = 'La classe "%s" contient trop de références à d\'autres classes : %d références (max %d)';
                    $this->warning($stat['line'], sprintf($msg, $name, count($stat['references']), $maxClassRef));
                } elseif($mediumClassRef !== false and count($stat['references']) > $mediumClassRef) {
                    $msg = 'La classe "%s" contient beaucoup de références à d\'autres classes : %d références';
                    $this->info($stat['line'], sprintf($msg, $name, count($stat['references'])));
                }
                $nbMethods = count( $stat['methods']);
                if( $nbMethods > 0 ) {
                    $cyclo = $stat['cyclo'] / $nbMethods;
                    if( $maxClassCyclo !== false and $cyclo > $maxClassCyclo) {
                        $msg = 'La moyenne des complexités cyclomatiques de la classe "%s" est trop élevée : %.1f (max %d)';
                        $this->warning($stat['line'], sprintf($msg, $name, $cyclo, $maxClassCyclo));
                    } elseif($mediumClassCyclo !== false and $cyclo > $mediumClassCyclo) {
                        $msg = 'La moyenne des complexités cyclomatiques de la classe "%s" est assez élevée : %.1f';
                        $this->info($stat['line'], sprintf($msg, $name, $cyclo));
                    }
                }
            }
        }
    }
    
    private function processIf() {
        $this->sendMessage('addCyclo');
        $this->checkParenthesis();
    }
    
    private function processFor() {
        $this->sendMessage('addCyclo');
        $this->checkParenthesis();
    }
    
    private function processForeach() {
        $this->checkParenthesis();
    }
    
    private function processWhile() {
        $this->sendMessage('addCyclo');
        $this->checkParenthesis();
    }
    
    private function processCase() {
        $this->sendMessage('addCyclo');
    }
    
    
    private function getOpenParenthesis($offset = 0) {
        do {
            $token = $this->getToken( $offset++);
        } while($token[0] !== 0 and $token[1] !== '(');
        return --$offset;
    }
    
    private function getCloseParenthesis($offset = 0) {
        $level = 1;
        do {
            $token = $this->getToken( $offset++);
            if( $token[0] === 0 ) {
                if( $token[1] === '(') $level++;
                elseif( $token[1] === ')') $level--;
            }
        } while( $level > 0);
        return --$offset;
    }
    
    private function checkParenthesis() {
        $offset = $this->getOpenParenthesis(0);
        if( $this->checkConf('controls', 'no.space.before.parenthesis') and $offset > 1) {
            $ctl = $this->getToken();
            $this->error($ctl[2], 'Il ne faut pas d\'espace entre "'.$ctl[1].'" et la parenthèse ouvrante');
        }

        $offset = $this->getCloseParenthesis($offset + 1) + 1;
        $token = $this->getToken( $offset );
        $isSpace = $token[1] === ' ' or $token[1] === ';';
        if( $this->checkConf('controls', 'space.after.parenthesis') and ! $isSpace ) {
            $ctl = $this->getToken();
            $this->error($token[2], 'Il manque un espace après la parenthèse fermante du "'.$ctl[1].'"');
        }
    }
    
    private function parseInclude() {
        $token = $this->getNextToken();
        $string = '';
        while( $token[1] !== ';') {
            $string .= $token[1];
            $token = $this->getNextToken(1, false);
        }
        $this->sendMessage('addInclude', trim($string));        
    }


    private function getToken( $offset = 0 ) {
        $token = $this->tokens[$this->index + $offset];
        if( ! is_array( $token ) ) {
            $token = array(0, $token, $this->currentLine);
        }
        return $token;
    }

    private function getNextToken( $offset = 1, $skipWhitespace = true) {
        do {
            $this->index += $offset;
            $token = $this->getToken();
        } while( $this->index < $this->lenght - 1 and $skipWhitespace and $token[0] === T_WHITESPACE);
        return $token;
    }




    private function addLine($token) {
        $this->currentLine = $token[2];
        $this->sendMessage('addLines');
    }
    
    private function isAssignment() {
        $i = 1;
        do {
            $token = $this->getToken($i++); 
        } while( $token[0] === T_WHITESPACE );
        if( $token[0] === 0 and $token[1] === '=') return true;
        switch( $token[0]) {
            case T_AND_EQUAL:
            case T_CONCAT_EQUAL:
            case T_DIV_EQUAL:
            case T_MINUS_EQUAL:
            case T_MOD_EQUAL:
            case T_MUL_EQUAL:
            case T_OR_EQUAL:
            case T_PLUS_EQUAL:
            case T_SL_EQUAL:
            case T_SR_EQUAL:
            case T_XOR_EQUAL:
                return true;
        }
        return false;
    }

    private function processString( $token ) {
        $i = 1;
        do {
            $t = $this->getToken($i);
            $i++;
        } while( $t[0] === T_WHITESPACE);
        if( $t[1] === '(' or $t[0] === T_DOUBLE_COLON) return;
        $i = -1;
        do {
            $t = $this->getToken($i);
            $i--;
        } while( $t[0] === T_WHITESPACE);
        if( $t[0] === T_OBJECT_OPERATOR ) return;
        
        $string = $token[1];
        if( preg_match('`^(true|false|null)$`i', $string)) {
            if($this->checkConf('constantes', 'reserve.lowercase') and !preg_match('`^(true|false|null)$`', $string)) {
                $this->error($token[2], 'Les constantes "true", "false" et "null" doivent être en minuscule : ' . $string);
            }
            return;
        } 
        if( preg_match('`^[A-Z0-9_]+$`i', $string)) {
            if($this->checkConf('constantes', 'name.uppercase') and !preg_match('`^[A-Z0-9_]+$`', $string)) {
                $this->error($token[2], 'Les constantes doivent être en majuscule : ' . $string);
            }
            return;
        } 
    }
    
    private function processVariable( $token ) {
        $var = $token[1];
        $len = strlen($var) - 1;
        $max = $this->checkConf('variables', 'name.max.length');
        if( false !== $max) {
            if( $len > $max) {
                $this->error($token[2], 'Nom de variable trop long (max ' .$max. ') : ' . $var);
            }
        }

        if( $this->checkConf('variables', 'name.first.lowercase') and
            preg_match('`^[a-z]`', $var)) {
                $this->error($token[2], 'Le nom d\'une variable doit commencer par une lettre minuscule : ' . $var);
        }
        
        switch( $var ) {
            case '$this' :
                $this->sendMessage('variableThis');
                break;
            case '$_GET':
            case '$_POST':
            case '$_COOKIE':
            case '$_SERVER':
                $this->sendMessage('variableSuperglobal');
                break;
            default:
                if(null !== $this->functionIndent) {
                    if( $this->isAssignment()) $this->sendMessage('variableLocalAssign', $var);
                    else $this->sendMessage('variableLocal', $var);
                } 
        }

    }

    private function addComment($token) {
        $nb = substr_count(trim($token[1]), "\n") + 1;
        $this->sendMessage('addComment', $nb);
    }

    private function processUnIndent() {
        //echo "$this->indent : $this->classIndent / $this->functionIndent\n";
        if( null !== $this->functionIndent and $this->indent === $this->functionIndent) {
            if( $this->classIndent !== null) {
                $this->sendMessage('leaveMethod');   
            } else {
                $this->sendMessage('leaveFunction');    
            }
            $this->functionIndent = null;
        } elseif( null !== $this->classIndent and $this->indent === $this->classIndent) {
            $this->sendMessage('leaveClass');
            $this->classIndent = null; 
        }
    }

    private function getPhpDoc( $comment ) {
        preg_match_all('`@(\w+)\s+(.*)$`m', $comment, $matches, PREG_SET_ORDER);
        $result = array();
        foreach( $matches as $match) {
            if( ! isset($result[$match[1]])) {
                $result[$match[1]] = array();
            }
            $result[$match[1]][] = trim( $match[2] );
        }
        return $result;
    }

    private function parseParameters() {
        $parameters = array();
        $parameter = array();
        $parenthese = 0;
        $token = $this->getNextToken();
        if( $token[1] !== '(') return array();

        do {
            $token = $this->getNextToken();
            if( $token[1] === '(') $parenthese++;
            elseif( $token[1] === ')') {
                $parenthese--;
                if( $parenthese < 0 ) break;
            }

            if( $token[1]=== ',' and $parenthese === 0) {
                $parameters[] = $parameter;
                $parameter = array();
            } elseif( $token[0] === T_STRING and empty( $parameter['variable']) ) {
                $parameter['typehint'] = $token[1];
            } elseif( $token[0] === T_VARIABLE ) {
                $parameter['variable'] = $token[1];
            } elseif( $token[1] === '=') {
                $parameter['default'] = true;
            }
        } while( true );
        if( ! empty( $parameter ) ) $parameters[] = $parameter;
        return $parameters;
    }

    private function addStaticClassReference() {
        $i = -1;
        do {
            $token = $this->getToken($i);
            $i--;
        } while( $token[0] === T_WHITESPACE );

        switch( strtolower($token[1])) {
            case 'self':
            case 'parent':
                break;
            default:
                $this->checkClassName($token);
                $this->sendMessage('addStaticReference', $token[1]);
        }
    }

    private function addNewClassReference() {
        $token = $this->getNextToken();
        // On n'est pas en mesure de trouver précisement le nom d'une classe
        // embarquer dans une variable
        if( $token[0] === T_VARIABLE ) return;
        if($token[0] === T_STRING) {
            $this->checkClassName($token);
            $this->sendMessage('addNewClassReference', $token[1]);
        }
    }

    private function addCatchClassReference() {
        $token = $this->getNextToken();
        if( $token[1] !== '(') return;
        $token = $this->getNextToken();
        if( $token[0] !== T_STRING) return;
        $this->checkClassName($token);
        $this->sendMessage('addCatchClassReference', $token[1]);
    }
    
    private function checkClassName($token) {
        if( $this->checkConf('classes', 'name.first.uppercase') and ! preg_match('`^[A-Z]`', $token[1])) {
            $this->error($token[2], 'Le nom d\'une class doit commencer par une lettre majuscule');
        }
    }

    private function enterFunction() {
        $token = $this->getNextToken();
        $function = $token[1];
        $line = $token[2];
        
        if( $this->indent > 0 ) {
            $this->error($line, 'La déclaration de la function ' . $function . ' n\'est pas au premier niveau d\'indentation du fichier');
        }

        $this->functionIndent = $this->indent;
        
        $index = $this->index;
        $token = $this->getNextToken(-1);
        if( $token[0] === T_DOC_COMMENT) {
            $doc = $this->getPhpDoc($token[1]);
        } else {
            $doc = null;
        }

        $this->index = $index;
        $parameters = $this->parseParameters();
        
        $this->sendMessage('enterFunction', array(
            'function' => $function,
            'line' => $line,
            'phpdoc' => $doc,
            'parameters' => $parameters
        ));
        
        foreach( $parameters as $parameter) {
            if( isset( $parameter['typehint'] ) ) {
                $this->sendMessage('addFunctionTypeHint', $parameter['typehint']);
            }
        }
    }

    private function enterMethod() {
        static $declarations = array(
            T_PUBLIC => array('var' => 'visibility', 'name' => 'public', 'order' => 0),
            T_PROTECTED => array('var' => 'visibility', 'name' => 'protected', 'order' => 0),
            T_PRIVATE => array('var' => 'visibility', 'name' => 'private', 'order' => 0),
            T_FINAL => array('var' => 'override', 'name' => 'final', 'order' => 1),
            T_ABSTRACT => array('var' => 'override', 'name' => 'abstract', 'order' => 1),
            T_STATIC => array('var' => 'access', 'name' => 'static', 'order' => 2)
        );

        $token = $this->getNextToken();
        $method = $token[1];
        $line = $token[2];

        if( $this->indent > 1 ) {
            $this->error($line, 'La déclaration de la méthode ' . $method . ' n\'est pas au premier niveau d\'indentation de la classe');
        }

        $this->functionIndent = $this->indent;

        $index = $this->index;

        $declaration = array();
        $lastOrder = -1;
        $this->getNextToken(-1);
        for( $i = 0; $i < 3; $i++) {
            $token = $this->getNextToken(-1);
            if( isset($declarations[$token[0]] )) {
                $declaration[$declarations[$token[0]]['var']] = $declarations[$token[0]]['name'];

                if($this->checkConf('methods', 'check.declaration.order') and $declarations[$token[0]]['order'] < $lastOrder ) {
                    $msg = 'Ordre de déclaration de la méthode "%s" n\'est pas correcte. [static] [final|abstract] (public|protected|private)';
                    $this->error($line, sprintf($msg, $method));
                    $lastOrder = -1;
                } else {
                    $lastOrder = $declarations[$token[0]]['order'];
                }
            } else {
                break;
            }
        }

        if( $this->checkConf('methods', 'check.visibility') and ! isset( $declaration['visibility'] ) ) {
            $this->error($line, 'La visibilité (public, protected, private) de la méthode ' . $method . ' n\'est pas définie.');
        }
        
        if( $token[0] === T_DOC_COMMENT) {
            $doc = $this->getPhpDoc($token[1]);
        } else {
            $doc = null;
        }

        $this->index = $index;
        $parameters = $this->parseParameters();
        
        $this->sendMessage('enterMethod', array(
            'method' => $method,
            'line' => $line,
            'declaration' => $declaration,
            'phpdoc' => $doc,
            'parameters' => $parameters
        ));
        
        
        foreach( $parameters as $parameter) {
            if( isset( $parameter['typehint'] ) ) {
                $this->sendMessage('addMethodTypeHint', $parameter['typehint']);
            }
        }

    }

    private function enterClass() {
        $token = $this->getNextToken();
        $className = $token[1];
        $line = $token[2];
        if( $this->indent > 0 ) {
            $this->error($token[2], 'La déclaration de la classe ' . $className . ' n\'est pas au premier niveau d\'indentation.');
        }
        $this->classIndent = $this->indent;
        $this->sendMessage('enterClass', array('name' => $className, 'line' => $line));
        $token = $this->getNextToken();
        if( $token[0] === T_EXTENDS) {
            $token = $this->getNextToken();
            if( $token[0] === T_STRING ) {
                $this->sendMessage('addExtends', $token[1]);
            }
            $token = $this->getNextToken();
        }
        if( $token[0] === T_IMPLEMENTS) {
            do {
                $token = $this->getNextToken();
                if($token[0] !== T_STRING) break;
                $this->sendMessage('addImplements', $token[1]);
                $token = $this->getNextToken();
                if( $token[1] !== ',') {
                    break;
                }
            } while(true);
        }
        $this->index--;
    }
    
    public function getFileStats() {
        return $this->fileParse->getStats();
    }

}

/*$tokens = token_get_all( file_get_contents('C:\web\frontendpc_2424actu\system\classes\Utils.class.php'));
 $f = fopen('result.txt', 'w');
 foreach ($tokens as $token) {
 $name = is_array($token) ? token_name($token[0]) : 'null';
 $data = is_array($token) ? $token[1] : $token;
 $line = is_array($token) ? $token[2] : '?';
 $data = str_replace("\n", '\n', $data);
 $data = str_replace("\r", '\r', $data);
 $data = str_replace("\t", '\t', $data);
 fputs($f, "[$line] $name: $data\n");
 }
 fclose($f);*/


$p = new Parser();
//$p->load('C:\web\frontendpc_2424actu\system\classes\Utils.class.php');
$p->load(__FILE__);
$p->parse();
$p->findErrors();
foreach( $p->getErrors() as $error) {
    if( $error[0] === Parser::LEVEL_ERROR) $level = 'ERROR';
    elseif($error[1] === Parser::LEVEL_ERROR) $level = 'WARNING';
    else $level = 'INFO';
    printf("%-7s : [%5d] %s\n", $level, $error[1], $error[2]);
}
//print_r($p->getFileStats());


